/**
 * 
 */
package com.sidom.movieIndexer.service.imdb;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.xpath.XPath;

import com.sidom.movieIndexer.domain.imdb.Cast;
import com.sidom.movieIndexer.domain.imdb.ImdbResult.Label;

/**
 * Utility class used to Screen scrape the imdb html.
 * @author Siebe
 */
public class ImdbScreenScraper {
	/**
	 * Xpath expression to identify a label component on IMDb.
	 */
	private static final String LABEL_IDENTIFIER_XPATH = "./h5";
	/**
	 * Xpath expression to identify a label component on IMDb.
	 */
	private static final String CAST_LABEL_IDENTIFIER_XPATH = "./div[@class='headerinline']/h3";
	/**
	 * Xpath expression to identify an a element in a info-content div.
	 */
	private static final String INFO_CONTENT_DIV_XPATH = "./div[@class='info-content']";
	/**
	 * Xpath expression to identify an a element in a info-content div.
	 */
	private static final String A_ELEMENTS_INFO_CONTENT_DIV_XPATH = INFO_CONTENT_DIV_XPATH + "/a";
	/**
	 * Xpath expression to identify an a plot keyword in a info-content div.
	 */
	private static final String PLOT_KEYWORDS_INFO_CONTENT_DIV_XPATH = INFO_CONTENT_DIV_XPATH + "/div[@id='tn15plotkeywords']/span/a";
	/**
	 * Xpath expression to identify a cast member in a info-content div.
	 */
	private static final String CAST_INFO_CONTENT_DIV_XPATH = INFO_CONTENT_DIV_XPATH + "/table[@class='cast']//tr";
	/**
	 * Words that need to be removed from the content {@link String}.
	 */
	private static final List<String> wordsToRemove = Arrays.asList("more", "|");
	
	/**
	 * Logger for {@link ImdbScreenScraper}.
	 */
	private static Log log = LogFactory.getLog(ImdbScreenScraper.class);
	
	/**
	 * Get content div from the IMDb html.
	 * <div id="tn15content">
	 * @param document The document.
	 * @return the contentDiv element.
	 */
	public static Element getContentDiv(Document document){
		Element contentDiv = getSingleNode(document, "//div[@id='tn15content']", "Failed to get the content div from the document");
		return contentDiv;
	}
	
	/**
	 * Get all the divs from the content div with class {@link Attribute}: "info"
	 * <div class="info">
	 * @param document The contentDivElement.
	 * @return A {@link List} of infoDiv {@link Element}s.
	 */
	public static List<Element> getInfoDivs(Element contentDivElement){
		List<Element> infoDivs = getNodes(contentDivElement, "./div[@class='info']", "Failed to get the info divs from the document");
		return infoDivs;
	}
	
	/**
	 * Get the Movie title from the document.
	 * @param document The {@link Document} to search the title in.
	 * @return The title of the Movie.
	 */
	public static String getTitle(Document document){
		String title = getTextFromSingleNode(document, "//title", "Could not get title from document");
		return StringUtils.substring(title, 0, StringUtils.indexOf(title, "(")).trim();
	}
	
	/**
	 * Get the Movie year from the document.
	 * @param document The {@link Document} to search the year in.
	 * @return The year of the Movie.
	 */
	public static String getYear(Document document){
		String year = getTextFromSingleNode(document, "//title", "Could not get year from document");
		return StringUtils.substring(year, StringUtils.indexOf(year, "(")+1, StringUtils.indexOf(year, ")"));
	}
	
	/**
	 * Get the cover url from the document.
	 * @param document The {@link Document} to search the cover url in.
	 * @return The cover url of the Movie.
	 */
	public static String getCoverUrl(Document document){
		Element coverUrl = getSingleNode(document, "//div[@id='tn15lhs']/div[@class='photo']/a/img", "Could not get cover url from document");
		if(coverUrl!=null){
			return cleanString(coverUrl.getAttributeValue("src"));
		}
		return "";
	}
	
	/**
	 * Get the user rating from the document.
	 * @param document The {@link Document} to search the user rating in.
	 * @return The user rating of the Movie.
	 */
	public static String getUserRating(Document document){
		return getTextFromSingleNode(document, "//div[@class='starbar-meta']/b", "Could not get the user rating from document");
	}
	
	/**
	 * Get the text content from the info div {@link Element} 
	 * where the text of the {@link ImdbScreenScraper#LABEL_IDENTIFIER_XPATH} matches the {@link Label#getLabel()}.
	 * @param infoDivs The {@link List} of infoDiv {@link Element} to search for.
	 * @param labels The {@link Label} you are looking for.
	 * @return The text content of that div.
	 * 		   Will return an empty {@link String} if something goes wrong.
	 */
	public static String getTextContentFromInfoDivs(List<Element> infoDivs, Label labels){
		for (Element infoElement : infoDivs) {
			String imdbLabel = getTextFromSingleNode(infoElement, LABEL_IDENTIFIER_XPATH, "Failed to get info label");
			if(!StringUtils.isEmpty(imdbLabel) && StringUtils.containsOnly(labels.getLabel(), imdbLabel.toLowerCase())){
				return getInfoContentDiv(infoElement);
			}
		}
		return "";
	}
	
	/**
	 * Get a {@link List} of text content from the info div {@link Element} 
	 * where the text of the {@link ImdbScreenScraper#LABEL_IDENTIFIER_XPATH} matches the {@link Label#getLabel()}.
	 * @param infoDivs The {@link List} of infoDiv {@link Element} to search for.
	 * @param labels The {@link Label} you are looking for.
	 * @return The a {@link List} of text content of that div.
	 * 		   Will return an empty {@link List} of {@link String}s if something goes wrong.
	 */
	public static List<String> getListContentFromInfoDivs(List<Element> infoDivs, Label labels){
		for (Element infoDivElement : infoDivs) {
			String imdbLabel = getTextFromSingleNode(infoDivElement, LABEL_IDENTIFIER_XPATH, "Failed to get info label");
			if(!StringUtils.isEmpty(imdbLabel) && StringUtils.containsOnly(labels.getLabel(), imdbLabel.toLowerCase())){
				return getTextFromNodes(infoDivElement, A_ELEMENTS_INFO_CONTENT_DIV_XPATH, "Failed to get info-content a tags");
			}
		}
		return new ArrayList<String>();
	}
	
	/**
	 * Get a {@link List} of plot keyword text content from the info div {@link Element} 
	 * where the text of the {@link ImdbScreenScraper#LABEL_IDENTIFIER_XPATH} matches the {@link Label#getLabel()}.
	 * @param infoDivs The {@link List} of infoDiv {@link Element} to search for.
	 * @param labels The {@link Label} you are looking for.
	 * @return The a {@link List} of text content of that div.
	 * 		   Will return an empty {@link List} of {@link String}s if something goes wrong.
	 */
	public static List<String> getPlotKeyWordsListContentFromInfoDivs(List<Element> infoDivs, Label labels){
		for (Element infoDivElement : infoDivs) {
			String imdbLabel = getTextFromSingleNode(infoDivElement, LABEL_IDENTIFIER_XPATH, "Failed to get info label");
			if(!StringUtils.isEmpty(imdbLabel) && StringUtils.containsOnly(labels.getLabel(), imdbLabel.toLowerCase())){
				return getTextFromNodes(infoDivElement, PLOT_KEYWORDS_INFO_CONTENT_DIV_XPATH, "Failed to get plot keywords from info-content div");
			}
		}
		return new ArrayList<String>();
	}
	
	/**
	 * Get a {@link List} of {@link Cast} members content from the info div {@link Element} 
	 * where the text of the {@link ImdbScreenScraper#LABEL_IDENTIFIER_XPATH} matches the {@link Label#getLabel()}.
	 * @param infoDivs The {@link List} of infoDiv {@link Element} to search for.
	 * @param labels The {@link Label} you are looking for.
	 * @return The a {@link List} of text content of that div.
	 * 		   Will return an empty {@link List} of {@link String}s if something goes wrong.
	 */
	public static List<Cast> getCastListContentFromInfoDivs(List<Element> infoDivs, Label labels){
		List<Cast> castMembers = new ArrayList<Cast>();
		for (Element infoDivElement : infoDivs) {
			String imdbLabel = getTextFromSingleNode(infoDivElement, CAST_LABEL_IDENTIFIER_XPATH, "Failed to get cast info label");
			if(!StringUtils.isEmpty(imdbLabel) && StringUtils.containsOnly(labels.getLabel(), imdbLabel.toLowerCase())){
				List<Element> trElements = getNodes(infoDivElement, CAST_INFO_CONTENT_DIV_XPATH, "Failed to get plot keywords from info-content div");
				for (Element trElement : trElements) {
					String name      = getTextFromSingleNode(trElement, "./td[@class='nm']/a", "Could not find name of cast member");
					String character = getTextFromSingleNode(trElement, "./td[@class='char']/a", "Could not find character name of cast member");
					if(StringUtils.isEmpty(name)){
						name = getTextFromSingleNode(trElement, "./td[@class='nm']", "Could not find name of cast member");
					}
					if(StringUtils.isEmpty(character)){
						character = getTextFromSingleNode(trElement, "./td[@class='char']", "Could not find character name of cast member");
					}
					castMembers.add(new Cast(name, character));
				}
			}
		}
		return castMembers;
	}
	
	/**
	 * Get the div with class infoContent.
	 * @param infoDivElement The infoDivElement.
	 * @return The text of the infoContent div.
	 */
	private static String getInfoContentDiv(Element infoDivElement){
		String infoContentElement = getTextFromSingleNode(infoDivElement, INFO_CONTENT_DIV_XPATH, "Failed to get info-content.");
		if(infoContentElement!=null && !StringUtils.isEmpty(infoContentElement)){
			return infoContentElement;
		}else{
			infoContentElement = getTextFromSingleNode(infoDivElement, A_ELEMENTS_INFO_CONTENT_DIV_XPATH, "Failed to get info-content a tags.");
			if(infoContentElement!=null && !StringUtils.isEmpty(infoContentElement)){
				return infoContentElement;
			}
		}
		return "";
	}
	
	/**
	 * Get single {@link Element} node from the {@link Document} using an Xpath expression.
	 * @param document The {@link Document} to perform the Xpath expression on.
	 * @param xPathExpression The Xpath expression.
	 * @param errorMessage In case of a {@link JDOMException} what errorMessage should be shown.
	 * @return The single {@link Element} returned by performing the Xpath expression on the {@link Document}.
	 * 		   Will return null if an exception occurs or if the {@link Element} was not found.
	 */
	private static Element getSingleNode(Document document, String xPathExpression, String errorMessage){
		Element element = null;
		try {
			element = (Element)XPath.selectSingleNode(document, xPathExpression);
		} catch (JDOMException e) {
			log.error(errorMessage, e);
		}
		return element;
	}
	
	/**
	 * Get single {@link Element} node from a {@link Element} using an Xpath expression.
	 * @param element The {@link Element} to perform the Xpath expression on.
	 * @param xPathExpression The Xpath expression.
	 * @param errorMessage In case of a {@link JDOMException} what errorMessage should be shown.
	 * @return The single {@link Element} returned by performing the Xpath expression on the {@link Element}.
	 * 		   Will return null if an exception occurs or if the {@link Element} was not found.
	 */
	private static Element getSingleNode(Element element, String xPathExpression, String errorMessage){
		Element returnElement = null;
		try {
			returnElement = (Element)XPath.selectSingleNode(element, xPathExpression);
		} catch (JDOMException e) {
			log.error(errorMessage, e);
		}
		return returnElement;
	}
	
	/**
	 * Get multiple {@link Element} nodes from an {@link Element} using an Xpath expression.
	 * @param element The {@link Element} to perform the Xpath expression on.
	 * @param xPathExpression The Xpath expression.
	 * @param errorMessage In case of a {@link JDOMException} what errorMessage should be shown.
	 * @return A {@link List} of {@link Element}s returned by performing the Xpath expression on the {@link Element}.
	 * 		   Will return an empty {@link List} if an exception occurs or if the {@link Element} was not found.
	 */
	@SuppressWarnings("unchecked")//JDom doesn't use generics.
	private static List<Element> getNodes(Element element, String xPathExpression, String errorMessage){
		List<Element> elements = new ArrayList<Element>();
		try {
			elements = (List<Element>)XPath.selectNodes(element, xPathExpression);
		} catch (JDOMException e) {
			log.error(errorMessage, e);
		}
		return elements;
	}
	
	/**
	 * Get the text from a single {@link Element} using an Xpath expression.
	 * @param element The {@link Element} to perform the Xpath expression on.
	 * @param xPathExpression The Xpath expression.
	 * @param errorMessage In case of a {@link JDOMException} what errorMessage should be shown.
	 * @return The text String from the result of the xpath expression on the {@link Element}.
	 * 		   Will return an empty String if an exception occured or if the {@link Element} was not found.
	 */
	private static String getTextFromSingleNode(Element element, String xPathExpression, String errorMessage){
		Element elementToReturn = getSingleNode(element, xPathExpression, errorMessage);
		return cleanString(getTrimmedTextFromElement(elementToReturn));
	}
	
	/**
	 * Get the text from a single {@link Document} using an Xpath expression.
	 * @param document The {@link Document} to perform the Xpath expression on.
	 * @param xPathExpression The Xpath expression.
	 * @param errorMessage In case of a {@link JDOMException} what errorMessage should be shown.
	 * @return The text String from the result of the xpath expression on the {@link Document}.
	 * 		   Will return an empty String if an exception occured or if the {@link Document} was not found.
	 */
	private static String getTextFromSingleNode(Document document, String xPathExpression, String errorMessage){
		Element element = getSingleNode(document, xPathExpression, errorMessage);
		return cleanString(getTrimmedTextFromElement(element));
	}
	
	/**
	 * Get the text from a {@link List} of {@link Element}s using an Xpath expression.
	 * @param element The {@link Element} to perform the Xpath expression on.
	 * @param xPathExpression The Xpath expression.
	 * @param errorMessage In case of a {@link JDOMException} what errorMessage should be shown.
	 * @return A {@link List} of {@link String}s from the result of the xpath expression on the {@link Element}.
	 * 		   Will return an empty {@link ArrayList} if an exception occured or if the {@link Element} was not found.
	 */
	private static List<String> getTextFromNodes(Element element, String xPathExpression, String errorMessage){
		List<Element> elements = getNodes(element, xPathExpression, errorMessage);
		List<String> textFromNodes = new ArrayList<String>();
		if(elements!=null && !CollectionUtils.isEmpty(elements)){
			for (Element collectionElement : elements) {
				if(collectionElement!=null && !StringUtils.isEmpty(collectionElement.getText()) && !collectionElement.getText().toLowerCase().equals(wordsToRemove.get(0))){
					textFromNodes.add(collectionElement.getTextTrim());
				}
			}
		}
		return textFromNodes;
	}
	
	/**
	 * Get the trimmed text from the {@link Element}.
	 * @param element The {@link Element} to get the trimmed text from.
	 * @return The trimmed text {@link String}.
	 * 		   Returns an empty {@link String} if the element was null or the text was null or empty.
	 */
	private static String getTrimmedTextFromElement(Element element){
		if(element!=null && !StringUtils.isEmpty(element.getText())){
			return element.getTextTrim();
		}
		return "";
	}
	
	/**
	 * Cleans up a String:
	 * - Unescapes html
	 * - Remove unwanted words
	 * - Trims the String.
	 * @param string The dirty String
	 * @return The cleaned String.
	 */
	private static String cleanString(String string){
		if(string!=null){
			for (String word : wordsToRemove) {
				string = string.replace(word, "");
			}
			string = string.replace("&amp;#x27;", "'");
			string = string.replace("&apos;", "'");
			string = string.trim();
		}
		return string;
	}
}
